const { EventEmitter } = require('events');
const electron = require('electron');

const path = require('path');
const crypto = require('crypto');
const fs = require('fs-extra');
const fetch = require('cross-fetch');
const semver = require('semver');
const { spawnSync, execFile, execSync } = require('child_process');
const yaml = require('yaml');

const { downloadFile, niceBytes } = require('./download');

const { getGithubFeedURL } = require('./github-provider');
const { getGenericFeedURL } = require('./generic-provider');
const { newBaseUrl, newUrlFromBase } = require('./utils');

// const { getStartURL, getWindow, dispatchEvent } = require('./splash');

const { app, BrowserWindow, Notification } = electron;
const oneMinute = 60 * 1000;
const fifteenMinutes = 15 * oneMinute;

const getChannel = () => {
  const version = app.getVersion();
  const preRelease = semver.prerelease(version);
  if (!preRelease) return 'latest';

  return preRelease[0];
};

const getAppName = () => app.getName();

const computeSHA256 = (filePath) => {
  if (!fs.existsSync(filePath)) {
    return null;
  }
  const fileBuffer = fs.readFileSync(filePath);
  const sum = crypto.createHash('sha256');
  sum.update(fileBuffer);
  const hex = sum.digest('hex');
  return hex;
};

const isSHACorrect = (filePath, correctSHA) => {
  try {
    const sha = computeSHA256(filePath);
    return sha === correctSHA;
  } catch (e) {
    return false;
  }
};

const stripTrailingSlash = (str) => (str.endsWith('/')
  ? str.slice(0, -1)
  : str);

class DeltaUpdater extends EventEmitter {
  constructor(options) {
    super();
    this.autoUpdateInfo = null;
    this.logger = options.logger || console;
    this.autoUpdater = options.autoUpdater || require('electron-updater').autoUpdater;
    this.hostURL = options.hostURL || null;
    this.showNotification = false;
    this.smartDownloadInfo = null;

    if (app.isPackaged) {
      this.setConfigPath();
      this.prepareUpdater();
      this.appPath = stripTrailingSlash(path.dirname(app.getPath('exe')));
      this.appName = getAppName();
      // this.logger.info('[Updater] App path = ', this.appPath);
      this.logger.info('[Updater] App path = ' + this.appPath);
      this.logger.info('[Updater] xxx App path = ', this.appPath);
    }
  }

  setConfigPath() {
    const updateConfigPath = path.join(process.resourcesPath, 'app-update.yml');
    this.updateConfig = yaml.parse(fs.readFileSync(updateConfigPath, 'utf8'));
  }

  async guessHostURL() {
    if (!this.updateConfig) { return null; }

    let hostURL = null;
    try {
      switch (this.updateConfig.provider) {
        case 'github':
          hostURL = await getGithubFeedURL(this.updateConfig);
          break;
        case 'generic':
          hostURL = await getGenericFeedURL(this.updateConfig);
          break;
        default:
          hostURL = await this.computeHostURL();
      }
    } catch (e) { this.logger.error('[Updater] Guess host url error ', e); }
    if (!hostURL) {
      return null;
    }
    hostURL = newBaseUrl(hostURL);
    return hostURL;
  }

  async computeHostURL() {
    const provider = await this.autoUpdater.clientPromise;
    return provider.baseUrl.href;
  }

  async prepareUpdater() {
    const channel = getChannel();
    if (!channel) return;

    // this.logger.info('[Updater]  CHANNEL = ', channel);
    this.logger.info('[Updater]  CHANNEL = ' + channel);
    this.logger.info('[Updater] xxx CHANNEL = ', channel);
    this.autoUpdater.channel = channel;
    this.autoUpdater.logger = this.logger;

    this.autoUpdater.allowDowngrade = false;
    this.autoUpdater.autoDownload = false;
    this.autoUpdater.autoInstallOnAppQuit = false;

    this.deltaUpdaterRootPath = path.join(
      app.getPath('appData'),
      `../Local/${this.updateConfig.updaterCacheDirName}`,
    );

    this.updateDetailsJSON = path.join(this.deltaUpdaterRootPath, './update-details.json');
    this.deltaHolderPath = path.join(this.deltaUpdaterRootPath, './deltas');

    if (app.isPackaged && process.platform === 'darwin') {
      this.macUpdaterPath = path.join(this.deltaUpdaterRootPath, './mac-updater');
      this.hpatchzPath = path.join(this.deltaUpdaterRootPath, './hpatchz');
    }
  }

  checkForUpdates(resolve, reject) {
    this.logger.log('[Updater] Checking for updates...');
    if (!this.hostURL && this.updateConfig && this.updateConfig.provider === 'github') {
      // special case for github, we need to get the latest release as delta-win/mac.json is
      // hosted at the root of the new release eg:
      // https://github.com/${owner}/${repo}/releases/download/${latestReleaseTagName}/delta-{win/mac}.json

      getGithubFeedURL(this.updateConfig).then((hostURL) => {
        this.logger.log('[Updater] github hostURL = ', hostURL);
        this.hostURL = newBaseUrl(hostURL);
        this.autoUpdater.checkForUpdates();
      })
        .catch((err) => {
          // loads the app's current version.
          this.logger.error('[Updater] check for updates failed.');
          reject(err);
        });
    } else {
      this.autoUpdater.checkForUpdates();
    }
  }

  pollForUpdates(resolve, reject) {
    this.checkForUpdates(resolve, reject);
    // setInterval(() => {
    //   this.checkForUpdates(resolve, reject);
    // }, fifteenMinutes);
  }

  // ensureSafeQuitAndInstall() {
  //   this.logger.info('[Updater] Ensure safe-quit and install');
  //   app.removeAllListeners('window-all-closed');
  //   const browserWindows = BrowserWindow.getAllWindows();
  //   browserWindows.forEach((browserWindow) => {
  //     browserWindow.removeAllListeners('close');
  //     if (!browserWindow.isDestroyed()) {
  //       browserWindow.close();
  //     }
  //   });
  // }

  async writeAutoUpdateDetails({ isDelta, attemptedVersion }) {
    if (process.platform === 'darwin') return;

    const date = new Date();
    const data = {
      isDelta,
      attemptedVersion,
      appVersion: app.getVersion(),
      timestamp: date.getTime(),
      timeHuman: date.toString(),
    };
    try {
      await fs.writeJSON(this.updateDetailsJSON, data);
    } catch (e) {
      this.logger.error('[Updater] ', e);
    }
  }

  async getAutoUpdateDetails() {
    let data = null;
    try {
      data = await fs.readJSON(this.updateDetailsJSON);
    } catch (e) {
      this.logger.error(`[Updater] ${this.updateDetailsJSON} file not found`);
    }
    return data;
  }

  async setFeedURL(feedURL) {
    try {
      this.logger.log('[Updater] Setting Feed URL for native updater: ', feedURL);
      await this.autoUpdater.setFeedURL(feedURL);
    } catch (e) {
      this.logger.error('[Updater] FeedURL set error ', e);
    }
  }

  async downloadUpdate(){
    const updateDetails = await this.getAutoUpdateDetails();
    if (updateDetails) {
      this.logger.info('[Updater] Last Auto Update details: ', updateDetails);
      const appVersion = app.getVersion();
      this.logger.info('[Updater] Current app version ', appVersion);
      // if (updateDetails.appVersion === appVersion) {
      // 防止增量安装UAC确认取消时，再次更新时全量更新
      if (updateDetails.appVersion === appVersion && !updateDetails.isDelta) {
        this.logger.info(
          '[Updater] Last attempted update failed, trying using normal updater',
        );
        this.autoUpdater.downloadUpdate();
        return;
      }
    }

    this.doSmartDownload(this.smartDownloadInfo);
  }

  attachListeners(resolve, reject) {
    if (!app.isPackaged) {
      setTimeout(() => {
        resolve();
      }, 1000);
      return;
    }
    this.autoUpdater.removeAllListeners();
    this.pollForUpdates(resolve, reject);

    this.logger.log('[Updater] Attaching listeners');

    this.autoUpdater.on('checking-for-update', () => {
      this.logger.log('[Updater] Checking for update');
      this.emit('checking-for-update');
    });

    this.autoUpdater.on('error', (error) => {
      this.logger.error('[Updater] Error: ', error);
      this.emit('error', error);
      reject(error);
    });

    this.autoUpdater.on('update-available', async (info) => {
      // this.logger.info('[Updater] Update available ', info);
      // 有delta时优先进行增量更新，此处需要确认是否存在delta-win.json
      const deltaJsonInfo = await this.getDeltaJsonInfo();
      if(!deltaJsonInfo){
        // 获取是否强制更新标识
        const fullJsonInfo = await this.getFullJsonInfo(info.version);
        var isForceUpdate = false;
        var latestReleaseInfo = [];
        if(fullJsonInfo){
          isForceUpdate = fullJsonInfo.forceUpdate;
          latestReleaseInfo = fullJsonInfo.latestReleaseInfo;
        }
        // 添加是否强制更新标识
        this.smartDownloadInfo = {...info};
        this.smartDownloadInfo['forceUpdate'] = isForceUpdate;
        this.smartDownloadInfo['latestReleaseInfo'] = latestReleaseInfo;
        this.logger.info('[Updater] Update available --- all info');
        this.logger.info(this.smartDownloadInfo);
        // 判断为全量更新
        this.emit('update-available', this.smartDownloadInfo);
      }else{
        this.logger.info('[Updater] Update available --- deltaJsonInfo');
        this.logger.info(deltaJsonInfo);
        this.smartDownloadInfo = {...deltaJsonInfo};
        this.smartDownloadInfo['releaseNotes'] = info.releaseNotes;
        // 判断为增量更新
        this.emit('update-available', this.smartDownloadInfo);
      }
    });

    this.autoUpdater.on('download-progress', (info) => {
      this.emit('download-progress', info);
    });

    this.logger.info('[Updater] Added on quit listener');

    // app.on('quit', this.onQuit);

    this.autoUpdater.on('update-not-available', () => {
      this.logger.info('[Updater] Update not available');
      this.emit('update-not-available');
      resolve();
    });

    this.autoUpdater.on('update-downloaded', (info) => {
      // this.logger.info('[Updater] Update downloaded ', info);
      // 添加是否强制更新标识
      var newJsonInfo = {...info};
      newJsonInfo['forceUpdate'] = this.smartDownloadInfo['forceUpdate'];
      this.logger.info('[Updater] Update downloaded');
      this.logger.info(newJsonInfo);
      this.emit('update-downloaded', newJsonInfo);
      this.handleUpdateDownloaded(newJsonInfo, resolve);
    });
  }

  async getDeltaJsonInfo() {
    const deltaJSONUrl = this.getDeltaJSONUrl();
    let deltaJSON = null;
    let newDeltaJsonInfo = null;
    try {
      this.logger.info(`[Updater] Fetching delta JSON from ${deltaJSONUrl}`);
      const response = await fetch(deltaJSONUrl);
      if (response.status !== 200) {
        this.logger.error(
          `[Updater] Error fetching ${deltaJSONUrl}: ${response.status}`,
        );
      } else {
        deltaJSON = await response.json();
      }
    } catch (err) {
      this.logger.error('Fetch failed ', deltaJSONUrl);
    }

    if (!deltaJSON) {
      this.logger.error('[Updater] No delta found');
      return newDeltaJsonInfo;
    }

    const appVersion = app.getVersion();
    const deltaDetails = deltaJSON[appVersion];
    const latestVersion = deltaJSON['latestVersion'];
    const latestReleaseInfo = deltaJSON['latestReleaseInfo'];

    if (!deltaDetails) {
      this.logger.error('[Updater] No delta found for this version ', appVersion);
      return newDeltaJsonInfo;
    }

    const deltaURL = this.getDeltaURL({ deltaPath: deltaDetails.path });
    this.logger.info('[Updater] Delta URL ', deltaURL);

    const shaVal = deltaDetails.sha256;

    if (!shaVal) {
      this.logger.info(
        '[Updater] SHA of delta could not be fetched. Trying normal download',
      );
      return newDeltaJsonInfo;
    }
    if (process.platform === 'darwin') {
        // mac系统暂不考虑
        return newDeltaJsonInfo;
    }
    // 构造同全量更新latest.yml类似的json数据
    newDeltaJsonInfo = {
      version: latestVersion,
      files: [
        {
          url: deltaDetails.path,
          sha256: deltaDetails.sha256,
          size: deltaDetails.size,
        }
      ],
      path: deltaDetails.path,
      sha256: deltaDetails.sha256,
      releaseDate: deltaDetails.releaseDate,
      forceUpdate: deltaDetails.forceUpdate,
      latestReleaseInfo: latestReleaseInfo,
    }
    return newDeltaJsonInfo;
  }

  async getFullJsonInfo(latestVersion) {
    const fullJSONUrl = this.getFullSONUrl();
    let fullJSON = null;
    let newFullJsonInfo = null;
    try {
      this.logger.info(`[Updater] Fetching full JSON from ${fullJSONUrl}`);
      const response = await fetch(fullJSONUrl);
      if (response.status !== 200) {
        this.logger.error(
          `[Updater] Error fetching ${fullJSONUrl}: ${response.status}`,
        );
      } else {
        fullJSON = await response.json();
      }
    } catch (err) {
      this.logger.error('full json Fetch failed ', fullJSONUrl);
    }

    if (!fullJSON) {
      this.logger.error('[Updater] No full found');
      return newFullJsonInfo;
    }
    
    const fullDetails = fullJSON[latestVersion];

    if (!fullDetails) {
      this.logger.error('[Updater] No full found for this version ' + latestVersion);
      return newFullJsonInfo;
    }

    if (process.platform === 'darwin') {
        // mac系统暂不考虑
        return newFullJsonInfo;
    }
    // 构造同全量更新latest.yml类似的json数据
    newFullJsonInfo = {
      version: latestVersion,
      forceUpdate: fullDetails.forceUpdate,
      latestReleaseInfo: fullDetails.latestReleaseInfo,
    }
    return newFullJsonInfo;
  }

  async onQuit(event, exitCode) {
    this.logger.info('[Updater] onQuit');
    if (this.autoUpdateInfo) {
      this.logger.info('[Updater] On Quit ', this.autoUpdateInfo);
      if (this.autoUpdateInfo.delta) {
        if (process.platform === 'win32') {
          try {
            this.logger.log(this.autoUpdateInfo.deltaPath, [`/APPPATH="${this.appPath}"`, '/RESTART="0"']);
            execSync(`${this.autoUpdateInfo.deltaPath} /APPPATH="${this.appPath}" /RESTART="0"`, {
              stdio: 'ignore',
            });
          } catch (err) {
            this.logger.error('[Updater] Spawn error ', err);
          }
        }

        if (process.platform === 'darwin') {
          const command = `${this.macUpdaterPath} ${getAppName()} ${this.autoUpdateInfo.deltaPath} ${this.hpatchzPath}`;
          this.logger.info(
            '[Updater] Applying delta update on macOS on Quit ',
            command,
          );

          execFile(this.macUpdaterPath, [
            getAppName(),
            this.autoUpdateInfo.deltaPath,
            this.hpatchzPath,
          ]).unref();
        }
      } else {
        await this.applyUpdate(this.autoUpdateInfo.version, false);
      }
    } else {
      this.logger.info('[Updater] Quitting now. No update available');
    }
  }

  quitAndInstall(callback) {
    this.logger.info('[Updater] Quit and Install');

    if (!this.autoUpdateInfo) {
      this.logger.info('[Updater] No update available');
      // true同增量更新同样处理，直接退出
      callback && callback(true);
      return;
    }

    setTimeout(async () => {
      if (this.autoUpdateInfo.delta) {
        this.logger.info('[Updater] Applying delta update');
        await this.applyDeltaUpdate(
          this.autoUpdateInfo.deltaPath,
          this.autoUpdateInfo.version,
        );
        // true是增量更新
        callback && callback(true);
      } else {
        this.logger.info('[Updater] Applying full update');
        await this.applyUpdate(this.autoUpdateInfo.version, true);
        // false不是增量更新
        callback && callback(false);
      }
    }, 0);
  }

  async handleUpdateDownloaded(info, resolve) {
    this.autoUpdateInfo = info; // important to save this info for later
    if (this.showNotification) {
      this.logger.info('[Updater] No splash window found. Show notification only.');
      this.showUpdateNotification(this.autoUpdateInfo);
    }
    this.logger.info('[Updater] Triggering update');
    resolve();
    // this.quitAndInstall();
  }

  showUpdateNotification(info) {
    const notification = new Notification({
      title: `${getAppName()} ${info.version} is available and will be installed on exit.`,
      body: 'Click to apply update now.',
      silent: true,
    });
    notification.show();
    notification.on('click', () => {
      this.quitAndInstall();
    });
  }

  async boot({
    showNotification,
  }) {
    this.showNotification = showNotification;
    this.logger.info('[Updater] Booting');
    if (!this.hostURL) {
      this.hostURL = await this.guessHostURL();
    }
    return new Promise((resolve, reject) => {
      this.attachListeners(resolve, reject);
        resolve();
    }).then(() => {
      this.logger.info('[Updater] Booted');
    }).catch((err) => {
      this.logger.error('[Updater] Boot error ', err);
    });
  }

  getDeltaURL({ deltaPath }) {
    return newUrlFromBase(deltaPath, this.hostURL);
  }

  getDeltaJSONUrl() {
    const jsonFileName = process.platform === 'win32' ? 'delta-win.json' : 'delta-mac.json';
    return newUrlFromBase(jsonFileName, this.hostURL);
  }

  getFullSONUrl() {
    const jsonFileName = process.platform === 'win32' ? 'full-win.json' : 'full-mac.json';
    return newUrlFromBase(jsonFileName, this.hostURL);
  }

  async doSmartDownload({ version, releaseDate }) {
    const deltaDownloaded = (deltaPath) => {
      this.logger.info(`[Updater] Downloaded ${deltaPath}`);
      this.autoUpdater.emit('update-downloaded', {
        delta: true,
        deltaPath,
        version,
        releaseDate,
      });
    };

    let channel = getChannel();
    if (!channel) return;
    channel = channel === 'latest' ? 'stable' : channel;

    const appVersion = app.getVersion();

    const deltaJSONUrl = this.getDeltaJSONUrl();
    let deltaJSON = null;
    try {
      this.logger.info(`[Updater] Fetching delta JSON from ${deltaJSONUrl}`);
      const response = await fetch(deltaJSONUrl);
      if (response.status !== 200) {
        this.logger.error(
          `[Updater] Error fetching ${deltaJSONUrl}: ${response.status}`,
        );
      } else {
        deltaJSON = await response.json();
      }
    } catch (err) {
      this.logger.error('Fetch failed ', deltaJSONUrl);
    }

    if (!deltaJSON) {
      this.logger.error('[Updater] No delta found');
      this.autoUpdater.downloadUpdate();
      return;
    }

    const deltaDetails = deltaJSON[appVersion];

    if (!deltaDetails) {
      this.logger.error('[Updater] No delta found for this version ', appVersion);
      this.autoUpdater.downloadUpdate();
      return;
    }

    const deltaURL = this.getDeltaURL({ deltaPath: deltaDetails.path });
    this.logger.info('[Updater] Delta URL ', deltaURL);

    const shaVal = deltaDetails.sha256;

    if (!shaVal) {
      this.logger.info(
        '[Updater] SHA of delta could not be fetched. Trying normal download',
      );
      this.autoUpdater.downloadUpdate();
      return;
    }
    if (process.platform === 'darwin') {
      try {
        const macUpdaterURL = newUrlFromBase('mac-updater', this.hostURL);
        const hpatchzURL = newUrlFromBase('hpatchz', this.hostURL);
        this.logger.info('[Updater] Downloading mac-updater and hpatchz');
        this.logger.info(`${macUpdaterURL} and ${hpatchzURL}`);
        await downloadFile(macUpdaterURL, this.macUpdaterPath);
        await downloadFile(hpatchzURL, this.hpatchzPath);
        await fs.chmod(this.macUpdaterPath, '755');
        await fs.chmod(this.hpatchzPath, '755');
      } catch (err) {
        this.logger.error('[Updater] Error downloading updater helper files', err);
        this.autoUpdater.downloadUpdate();
        return;
      }
    }

    const deltaPath = path.join(this.deltaHolderPath, deltaDetails.path);

    if (fs.existsSync(deltaPath) && isSHACorrect(deltaPath, shaVal)) {
      // cached downloaded file is good to go
      this.logger.info('[Updater] Delta file is already present ', deltaPath);
      deltaDownloaded(deltaPath);
      return;
    }

    this.logger.info('[Updater] Start downloading delta file ', deltaURL);

    await fs.ensureDir(this.deltaHolderPath);

    const onProgressCb = ({ percentage, transferred, total }) => {
      this.logger.info(`downladed=${percentage}%, transferred = ${transferred} / ${total}`);
      this.emit('download-progress', { percentage, transferred, total });
    };

    try {
      await downloadFile(deltaURL, deltaPath, onProgressCb.bind(this));
      const isFileGood = isSHACorrect(deltaPath, shaVal);
      if (!isFileGood) {
        this.logger.info(
          '[Updater] Delta downloaded, SHA incorrect. Trying normal download',
        );
        this.autoUpdater.downloadUpdate();
        return;
      }
      deltaDownloaded(deltaPath);
    } catch (err) {
      this.logger.error('[Updater] Delta download error, trying normal download', err);
      this.autoUpdater.downloadUpdate();
    }
  }

  async applyUpdate(version, forceRunAfter = true) {
    this.logger.info('[Updater] Applying normal update');
    await this.writeAutoUpdateDetails({ isDelta: false, attemptedVersion: version });

    // this.ensureSafeQuitAndInstall();
    if (process.platform === 'darwin') {
      this.autoUpdater.quitAndInstall();
      return;
    }
    // setTimeout(() => this.autoUpdater.quitAndInstall(true, forceRunAfter), 100);
    // 在background.js的exit-and-update中处理
    // setTimeout(() => this.autoUpdater.quitAndInstall(), 100);
  }

  async applyDeltaUpdate(deltaPath, version) {
    this.logger.info('[Updater] Applying delta update');
    await this.writeAutoUpdateDetails({ isDelta: true, attemptedVersion: version });
    // this.ensureSafeQuitAndInstall();

    try {
      if (process.platform === 'darwin') {
        const command = `${this.macUpdaterPath} ${getAppName()} ${deltaPath} ${this.hpatchzPath}`;
        this.logger.info(
          '[Updater] Applying delta update with execFile ',
          command,
        );
        execFile(this.macUpdaterPath, [
          getAppName(),
          deltaPath,
          this.hpatchzPath,
        ]).unref();
      } else {
        this.logger.log(deltaPath, [`/APPPATH="${this.appPath}"`, '/RESTART="1"']);
        execSync(`${deltaPath} /APPPATH="${this.appPath}" /RESTART="1"`, {
          stdio: 'ignore',
        });
      }
      // 在background.js的exit-and-update中处理
      // app.removeListener('quit', this.onQuit);
      // app.isQuitting = true;
      // app.quit();
    } catch (err) {
      this.logger.info('[Updater] Apply delta error ', err);
    }
  }
}

module.exports = DeltaUpdater;
